Next.js 的 routing 跟一般常見的 react
+ react-route-dom
的組合不太一樣,是採用 file-based routing。在 Next.js 中最基本的單位是 page,一個 page 就是一個 react component,component 會被放置在 pages
的資料夾中,而其檔案名稱將會決定路由的名稱。
在 Next.js 可以分為三種的 routing 方式,分別為:
catch all routes 屬於 dynamic routes 的一種
首先是 static routes,舉一個例子,像是我們在前一天使用 create-next-app
產生的專案中,裡面有個 pages/index.tsx
,所以在瀏覽網頁時使用的路徑就會是 /
。再舉另一個例子,如果有個檔案是被放置在 pages/post.tsx
,而路徑就會是 /post
;如果是 page/post/index.tsx
跟前面一樣的意思,同樣路徑會是 /post
。
這是最基本定義 page 的方式,用資料夾層級的方式來決定 url 會長什麼樣子,使用這種方式就不用像是 react-router-dom
需要在檔案中定義路由,還可以讓整體的程式碼更乾淨。
要注意的是,如果是 component 是一個 page,則它必須用
default export
而不是named export
。
export default function Home({ post }: HomeProps) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}
當讀者看到這邊,肯定會疑惑像是 /post/<post-id>
這種 url 該怎麼定義呢? <post-id>
是動態的,每新增一篇貼文難道就要新增一個 component 嗎?這樣的設計未免太不人性化了吧!
Next.js 當然有相對應的解決方案,動態的 url 可以用 dynamic routes 來處理。這也跟 file-based routing 有關係,因為就是透過檔案名稱的定義方式來實現 dynamic routes,以 /post/<post-id>
這個 url 來說,可以用 [postId].tsx
定義一個 page 的名稱來達成 dynamic routes。
以下是 /pages/post/[postId].tsx
的範例程式:
import { useRouter } from "next/router";
const Post = () => {
const router = useRouter();
const { postId } = router.query;
return <p>Post: {postId}</p>;
};
export default Post;
在預設的情況下,像是 /post/123
或是 /post/abc
皆可以滿足 /pages/post/[postId].tsx
,而 123
跟 abc
就會被當作是 url 的 query string,被併入到 router.query
裡面。
舉例來說, /post/123
併入到 router.query
的物件如下:
{
postId: 123;
}
如果在 url 加上一個 query string,例如 /post/123?hello=world
,則 router.query
這個物件就會長得像這個樣子:
{ postId: 123, hello: 'world' }
而這邊要特別注意的是,假設 query string 跟路由的名稱一樣,例如 /post/123?postId=abc
,則 abc
會被 123
蓋過去,最終得到的結果如下:
{
postId: 123;
}
多層級的路由一樣,也是透過 file-based routing 的方式定義,例如以 /post/123/abc
來說,我們就可以定義 /pages/post/<postId>/<commentId>.tsx
來匹配這個路由,在這個層級,也可以拿到前幾個層級的參數,會被統一合併到 router.query
中:
{ postId: 123, commentId: 'abc' }
有時候在設計 url 時會遇到一種情況,並必須要在每一個層級都有代表的 component,雖然這樣很彈性,但是就要花費比較多心思撰寫更多的 component。
同樣用上方 post 的例子來說,有時候會希望 post 的 url 能夠以「年月日」來設計,所以一篇 post 的設 url 會設計成這個樣子 /pages/post/<year>/<month>/<day>
,接下來讀者可能會頭痛了,難道要定義多層級的資料夾嗎?而且最後可能還只有一個 /pages/.../day.tsx
,這樣感覺挺麻煩的。
pages/
└── posts/
└── [year]/
└── [month]/
└── [day].js
這個問題 Next.js 也有相對應的解決方案,可以使用官方稱作為「catch all routes」的定義方式,一次拿到所有層級的參數。
以上方的例子,只要定義 /pages/[...date].tsx
就可以匹配「年月日」的參數,而且甚至可以無限地加上新的參數,例如顆粒度想要細到小時、分鐘、秒,都是可以的,因為 [...date]
的資料最後會以陣列被儲存在 router.query
中,例如 /post/2021/12/31
會拿到以下這個物件:
{
date: [2021, 12, 31];
}
而再把 post 的顆粒度在切得更小的話,因為可能一天不止一篇貼文,像是 /post/2021/12/31/12/30/00
,最終就可以拿到這樣子的物件:
{
date: [2021, 12, 31, 12, 30, 00];
}
Next.js 是一個很棒的框架,提供了完善的 file-based routing,可以用三種不同的 pattern 定義路由的規則。但是,在使用 router.query
時要注意「第一次 render 時拿不到值」的問題,因為 Next.js 有 Automatic Static Optimization 的機制,在第一個階段 (第一次渲染) 會先執行 pre-rendering 產生靜態的 HTML,這時候 router.query
會是空的 {}
,在第二個階段 (第二次渲染) 時才能夠從 router.query
中拿到值。
以下方這個範例來說,我們可以嘗試在 pages/post/[postId].tsx
中簡單地用 console.log
檢查 postId
是否有值。
import { useRouter } from "next/router";
const Post = () => {
const router = useRouter();
const { postId } = router.query;
console.log(postId);
return <p>Post id: {postId}</p>;
};
export default Post;
從結果上來看, postId
沒辦法在第一階段時就拿到值,如果想要操作 postId
就要特別小心這個問題。
在 Next.js 官方的 GitHub 上也有些人在討論這個問題,在看過大家的討論後,筆者整理了三種可行的解法,我們同樣用上從 postId
的情境。
判斷 postId
有沒有值
const { postId } = router.query;
if (!postId) {
return <Loading />;
}
使用 isReady
判斷是否能從 useRouter()
中取值 (官方不推薦將 isReady
使用在 conditionally rendering,只能被用於 useEffect
中)
const { query, isReady } = useRouter();
if (!isReady) {
return <Loading />;
}
從另一個參數 asPath
中用 regex
取值
const { asPath } = useRouter();
const { postId } = asPath.match(/\/post\/(?<postId>.*)/);
前兩種方式是判斷是否能夠從 router.query
取得值,而第三種方式則是不用 Next.js 提方的方式,改用自己寫 regex 的方法取值,但如果非必要得在第一次渲染時就取得值,否則不推薦使用第三種方式。
File-based routing | Code-based routing |
---|---|
Next.js | react + react-router-dom |
不需要在程式中定義 routing | 需要在程式中設定 <Router> 、 <Switch> ... |
直覺地用檔案階層定義 routing | 檔案放的位置不必按照規範,可能 Route 會出現在意料之外的地方 |
使用 <Link> 作為 routing 的 component |
使用 <Link> 作為 routing 的 component |